package ar.uba.fi.tonyvaliente.signature.process;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

import org.apache.commons.io.FileUtils;
import org.htmlparser.NodeFilter;
import org.htmlparser.Parser;
import org.htmlparser.filters.TagNameFilter;
import org.htmlparser.util.NodeIterator;
import org.htmlparser.util.NodeList;
import org.htmlparser.util.ParserException;

import ar.uba.fi.tonyvaliente.documents.CreateIndexResult;
import ar.uba.fi.tonyvaliente.documents.DocumentHashing;
import ar.uba.fi.tonyvaliente.documents.DocumentIndex;
import ar.uba.fi.tonyvaliente.documents.DocumentInfo;
import ar.uba.fi.tonyvaliente.documents.DocumentManager;
import ar.uba.fi.tonyvaliente.documents.Index;
import ar.uba.fi.tonyvaliente.documents.Query;
import ar.uba.fi.tonyvaliente.documents.QueryDocumentsResult;
import ar.uba.fi.tonyvaliente.documents.QueryFactory;
import ar.uba.fi.tonyvaliente.documents.exception.QueryException;
import ar.uba.fi.tonyvaliente.signature.HashFunction;
import ar.uba.fi.tonyvaliente.signature.HashFunctionException;
import ar.uba.fi.tonyvaliente.signature.files.SignatureIndex;
import ar.uba.fi.tonyvaliente.signature.query.SignatureQuery;
import ar.uba.fi.tonyvaliente.utils.MD5;
import ar.uba.fi.tonyvaliente.utils.RecursosAplicacion;

/**
 * Genera la firma de un documento.
 * Para realizar su tarea recibe una referencia al documento (path), una lista
 * de funciones de hash a aplicar y la longitud de la firma de cada término.
 * Los resultados se almacenan en un archivo que contiene un identificador de documento
 * y la firma generada.
 * 
 * @author santiago
 *
 */
public class SignatureGenerator {
	
	private static final String DOCS_ROOT = RecursosAplicacion.instance().getProperty("docs.root");
	
	public static CreateIndexResult createIndex(List<HashFunction> hashFunctions, int length, int keys) {
		try {
			File directory = new File(DOCS_ROOT + "/");
			if (directory.exists()){
//				FileUtils.cleanDirectory(directory);
			}else{
				directory.mkdirs();
			}
			
			DocumentHashing documentHashing = new DocumentHashing();
			documentHashing.initilize();

			Index documentIndex = new SignatureIndex(hashFunctions, length);
			documentIndex.initialize(keys);
			documentIndex.close();

			return new CreateIndexResult(true, "Success!!!");
		} catch (Exception e) {
			e.printStackTrace();
			return new CreateIndexResult(false, e.getMessage());
		}
	}
	
	
	/**
	 * A cada término del documento almacenado en <code>filePath</code> le aplica
	 * las <code>hashFunctions</code> para generar una firma de <code>length</code> bits.
	 * 
	 * @param filePath
	 * @param hashFunctions
	 * @param length
	 */
	public static CreateSignatureResult process(String filePath, List<HashFunction> hashFunctions, int length){
		
		String sourcePath = filePath;
		String documentHash = MD5.calculateHash(filePath);
		String targetPath = documentHash;
		if (targetPath.equals(""))
			return new CreateSignatureResult(false, "MD5 invalid hash");

		try {
			File source = new File(sourcePath);
			File target = new File(DOCS_ROOT + "/" + targetPath.substring(0, 1) + "/"
					+ targetPath.substring(1, 2), targetPath);
			filePath = target.getAbsolutePath();

			if (target.exists()){
//				return new CreateSignatureResult(false, "Document already exists");
			}
			FileUtils.copyFile(source, target);
		} catch (Exception e) {
			e.printStackTrace();
			return new CreateSignatureResult(false, e.getMessage());
		}

		String title = sourcePath;
		NodeList bodyList = new NodeList();
		try {
			Parser parser = new Parser();
			NodeList tags;

			parser.setResource(filePath);
			tags = parser.parse(null);

			NodeFilter filter;

			NodeList headList = new NodeList();
			filter = new TagNameFilter("HEAD");
			for (NodeIterator it = tags.elements(); it.hasMoreNodes();)
				it.nextNode().collectInto(headList, filter);

			NodeList titleList = new NodeList();
			filter = new TagNameFilter("TITLE");
			for (NodeIterator it = headList.elements(); it.hasMoreNodes();)
				it.nextNode().collectInto(titleList, filter);

			if (titleList.size() > 0) {
				try {
					title = titleList.elementAt(0).getChildren().elementAt(0)
							.getText();
				} catch (Exception e) {
				}
			}

			filter = new TagNameFilter("BODY");
			for (NodeIterator it = tags.elements(); it.hasMoreNodes();)
				it.nextNode().collectInto(bodyList, filter);
		} catch (ParserException e) {
			e.printStackTrace();
			return new CreateSignatureResult(false, e.getMessage());
		}

		NodeList filteredNodes = DocumentManager.performNodeFiltering(bodyList);
		LinkedList<String> extractedText = DocumentManager.performTextExtraction(filteredNodes);
		LinkedList<String> filteredText = DocumentManager.performTextFiltering(extractedText);

		DocumentHashing documentHashing = new DocumentHashing();
		int documentId = documentHashing.addDocument(documentHash, title);
		
		List<BitSet> wordSignatures = new ArrayList<BitSet>(filteredText.size());
		
		try {
			for (String word : filteredText) {
//				List<Number> posiciones = new ArrayList<Number>();
				BitSet wordSignature = new BitSet(length);
				for (HashFunction hashFunction : hashFunctions) {
					// TODO si siempre vamos a castear a int mejor dejar que el método devuelva int y listo
					// sobre todo si el bitSet no soporta muchos bits.
					// Si se necesitan más bits hay que buscar una alternativa al bitSet
					Integer hashValue = hashFunction.hash(word).intValue();
					wordSignature.set(hashValue, true);
					wordSignatures.add(wordSignature);
				}
				
			}
			
			BitSet signature = new BitSet(length);
			for (BitSet wordSignature : wordSignatures) {
				signature.or(wordSignature);
			}

			Index documentIndex = new SignatureIndex(hashFunctions, length);
			documentIndex.open();
			documentIndex.add(signature, documentId);
			documentIndex.close();
			return new CreateSignatureResult(true, "success!!");
			
		} catch (HashFunctionException e) {
			e.printStackTrace();
			return new CreateSignatureResult(false, e.getMessage());
		} catch (Exception e) {
			e.printStackTrace();
			 return new CreateSignatureResult(false, e.getMessage());
		}

	}
	
	public static QueryDocumentsResult queryDocuments(String query,
			boolean showDetails, List<HashFunction> hashFunctions, int length) {
		LinkedList<String> originalText = new LinkedList<String>();
		originalText.add(query);
		LinkedList<String> filteredText = DocumentManager.performTextFiltering(originalText);

		HashMap<String, List<Integer>> documentsByTerm = new HashMap<String, List<Integer>>();

		Index documentIndex = new SignatureIndex(hashFunctions, length);
		documentIndex.open();
		for (String term : filteredText) {
			if (documentsByTerm.containsKey(term))
				continue;
			try {
				documentsByTerm.put(term, documentIndex.getDocuments(term));
			} catch (QueryException e) {
				e.printStackTrace();
				return new QueryDocumentsResult();
			}
		}
		documentIndex.close();

		// TODO: ahora solo hace interseccion de las listas (AND)
		boolean emptyDocumentList = false;
		HashMap<String, Integer> terms = new HashMap<String, Integer>();
		for (String term : documentsByTerm.keySet()) {
			List<Integer> documents = documentsByTerm.get(term);
			if (documents.size() == 0) {
				emptyDocumentList = true;
				break;
			}
			terms.put(term, documents.remove(0));
		}
		if (emptyDocumentList) {
			return new QueryDocumentsResult();
		}

		LinkedList<Integer> documentList = new LinkedList<Integer>();

		boolean stop = false;
		while (!stop) {

			int minDocumentId = Integer.MAX_VALUE;
			String minTerm = null;
			for (String term : terms.keySet()) {
				Integer documentId = terms.get(term);
				if (documentId < minDocumentId) {
					minDocumentId = documentId;
					minTerm = term;
				}
			}

			boolean add = true;
			for (String term : terms.keySet()) {
				Integer documentId = terms.get(term);
				add = add && documentId.equals(minDocumentId);
			}
			if (add) {
				documentList.add(minDocumentId);

				for (String term : terms.keySet()) {
					List<Integer> documents = documentsByTerm.get(term);
					if (documents.size() == 0) {
						stop = true;
						break;
					}
					terms.put(term, documents.remove(0));
				}
			} else {
				List<Integer> documents = documentsByTerm.get(minTerm);
				if (documents.size() == 0) {
					stop = true;
					break;
				}
				terms.put(minTerm, documents.remove(0));
			}
		}

		DocumentHashing documentHashing = new DocumentHashing();
		QueryDocumentsResult result = new QueryDocumentsResult();
		for (Integer documentId : documentList) {
			result.addResult(documentId.intValue());

			DocumentInfo documentInfo = documentHashing
					.getDocumentInfo(documentId.intValue());
			StringBuffer message = new StringBuffer();
			message.append(documentInfo.getTitle());
			message.append(" (Document #");
			message.append(documentId);
			message.append(")");
			if (showDetails) {
				try {
					String path = documentInfo.getHash();
					File file = new File(DOCS_ROOT + "/" + path.substring(0, 1) + "/"
							+ path.substring(1, 2), path);
					if (file.exists()) {
						BufferedReader reader = new BufferedReader(
								new FileReader(file));
						String line;
						int appended = 0;
						while ((line = reader.readLine()) != null) {
							message.append(line);
							message.append("\n");
							appended++;
							if (appended == 6)
								break;
						}
					}
				} catch (Exception e) {
					e.printStackTrace();
				}

				message.append("\n");
			}
			message.append("\n");
			result.addMessage(message.toString());
		}

		return result;
	}
	
	/**
	 * Crea una nueva QueryFactory vinculada al indice
	 * de signature files.
	 * @return Nueva QueryFactory vinculada al indice
	 * de signature files
	 */
	private QueryFactory createQueryFactory() {
		return new SignatureQueryFactory();
	}

}

/**
 * QueryFactory vinculada al indice de signature files 
 * @author dmorello
 */
class SignatureQueryFactory implements QueryFactory {

	@Override
	public Query createBooleanQuery(String strQuery) throws QueryException {
		return SignatureQuery.parse(strQuery);
	}
}